﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Configuration;
using System.Data.SqlClient;
using Microsoft.Synchronization.Data;
using Microsoft.Synchronization.Data.SqlServer;
using Microsoft.Synchronization;
using System.Data;
using System.IO;

namespace UniSync.Core
{
    /// <summary>
    /// 
    /// </summary>
    public class AzureSync
    {
        #region == Publics Members ========================
        
        /// <summary>
        /// The connectionString to the main Database
        /// </summary>
        public String LocalConnectionString { get; set; }

        /// <summary>
        /// The connectionString to the AzureSql Database
        /// </summary>
        public String AzureConnectionString { get; set; }

        /// <summary>
        /// The Name of the Synchronisation scope
        /// </summary>
        public String ScopeName { get; set; }

        /// <summary>
        /// All the table names that need to be sync for the current ScopeName
        /// Separated by a comma
        /// </summary>
        public string[] TableNamesToSync { get; set; }

        #endregion

        #region == Constructor ============================

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="localConnectionString">Connection String to a MS SQL Database</param>
        /// <param name="azureConnectionString">Connection String to a SQLAzure Database</param>
        /// <param name="scopeName">The scope name of this sychronization</param> 
        /// <param name="tableNamesToSync">List of table name separated by a ","</param>
        public AzureSync(String localConnectionString, String azureConnectionString, String scopeName, String[] tableNamesToSync)
        {
            LocalConnectionString = localConnectionString;
            AzureConnectionString = azureConnectionString;
            ScopeName = scopeName;
            TableNamesToSync = tableNamesToSync;
        }

        #endregion

        #region == Publics Functions ======================
        
        /// <summary>
        /// Run once this will add the table and the tools needed
        /// BAse on the LocalConnection
        /// </summary>
        public void Setup()
        {

            using (var _sqlServerConn = new SqlConnection(LocalConnectionString))
            {
                using (var _sqlAzureConn = new SqlConnection(AzureConnectionString))
                {
                    var _scope = new DbSyncScopeDescription(ScopeName);
                    
                    // Get the table infos from the Local database
                    foreach (var _tblName in TableNamesToSync)
                    {
                        var _tblToSync = SqlSyncDescriptionBuilder.GetDescriptionForTable(_tblName, _sqlServerConn);
                        _scope.Tables.Add(_tblToSync);
                    }

                    // Prepare Local Provisioning
                    var _sqlServerProv = new SqlSyncScopeProvisioning(_sqlServerConn, _scope);
                    if (!_sqlServerProv.ScopeExists(ScopeName))
                        _sqlServerProv.Apply();

                    // Prepare Cloud Provisioning
                    var _sqlAzureProv = new SqlSyncScopeProvisioning(_sqlAzureConn, _scope);
                    if (!_sqlAzureProv.ScopeExists(ScopeName))
                        _sqlAzureProv.Apply();
                }
            }
        }
        
        /// <summary>
        /// Do the synchronisation
        /// </summary>
        public void Sync()
        {
            using (
                var _sqlServerConn = new SqlConnection(LocalConnectionString))
            {
                using (var _sqlAzureConn = new SqlConnection(AzureConnectionString))
                {
                    var localProvider = new SqlSyncProvider(ScopeName, _sqlAzureConn);
                    var remoteProvider = new SqlSyncProvider(ScopeName, _sqlServerConn);

                    localProvider.ChangesSelected += localProvider_ChangesSelected;
                    remoteProvider.ChangesSelected += remoteProvider_ChangesSelected;
                    localProvider.ApplyChangeFailed += localProvider_ApplyChangeFailed;
                    remoteProvider.ApplyChangeFailed += remoteProvider_ApplyChangeFailed;
                    
                    var syncOrchestrator = new SyncOrchestrator
                    {
                        LocalProvider = localProvider,
                        RemoteProvider = remoteProvider,
                        Direction = SyncDirectionOrder.UploadAndDownload
                    };

                    var syncStats = syncOrchestrator.Synchronize();
                }
            }
        }

        private void localProvider_ChangesSelected(object sender, DbChangesSelectedEventArgs e)
        {
        }

        void remoteProvider_ChangesSelected(object sender, DbChangesSelectedEventArgs e)
        {
        }

        private void localProvider_ApplyChangeFailed(object sender, DbApplyChangeFailedEventArgs e)
        {
            if (this.LocalApplyChangeFailed != null)
                this.LocalApplyChangeFailed(sender, e);

            LogConflict(e);
        }

        private void remoteProvider_ApplyChangeFailed(object sender, DbApplyChangeFailedEventArgs e)
        {
            if (this.AzureApplyChangeFailed != null)
                this.AzureApplyChangeFailed(sender, e);

            LogConflict(e);
        }

        /// <summary>
        /// Delete the metadata for the current Scope, older than the specified number of days
        /// </summary>
        /// <remarks>Cleanup is retention-based, which means that metadata that is older 
        /// than the specified number of days is deleted</remarks>
        public void CleanDataScopeOlderThan(int nbDays)
        {
            CleanDataScopeOlderThanByDB(this.LocalConnectionString, nbDays);
            CleanDataScopeOlderThanByDB(this.AzureConnectionString, nbDays);
        }
        
        /// <summary>
        /// Reset the MetaData for the current Scope.
        /// </summary>
        public void ScopeReset()
        {
            ScopeResetByDB(this.LocalConnectionString);
            ScopeResetByDB(this.AzureConnectionString);
            
            this.Setup();
        }

        /// <summary>
        /// Delete ALL table related to the Sync Framework
        /// <remarks>All scopes are affected, and metadata tables too.</remarks>
        /// </summary>
        public void DeleteSync() {
            DeleteSyncByBD(this.LocalConnectionString);
            DeleteSyncByBD(this.AzureConnectionString);
        }

        #endregion

        #region == Privates Functions =====================

        private void CleanDataScopeOlderThanByDB(String connectionString, int nbDays)
        {
            using (var _sqlConn = new SqlConnection(connectionString))
            {
                var _cleaner = new SqlSyncStoreMetadataCleanup(_sqlConn);
                _cleaner.RetentionInDays = nbDays;
                _cleaner.PerformCleanup();
            }
        }

        private void ScopeResetByDB(String connectionString)
        {
            using (var _sqlConn = new SqlConnection(connectionString))
            {
                var _cleaner = new SqlSyncScopeDeprovisioning(_sqlConn);
                _cleaner.DeprovisionScope(this.ScopeName);
            }
        }

        private void DeleteSyncByBD(String connectionString)
        {
            using (var _sqlConn = new SqlConnection(connectionString))
            {
                var _cleaner = new SqlSyncScopeDeprovisioning(_sqlConn);
                _cleaner.DeprovisionStore();
            }
        }


        private void LogConflict(DbApplyChangeFailedEventArgs e)
        {
            string DbConflictDetected = e.Connection.Database.ToString();

            DataTable conflictingRemoteChange = e.Conflict.RemoteChange;
            DataTable conflictingLocalChange = e.Conflict.LocalChange;
            int remoteColumnCount = conflictingRemoteChange.Columns.Count;
            int localColumnCount = conflictingLocalChange.Columns.Count;

            var _sb = new StringBuilder();
            _sb.Append("Row from " + DbConflictDetected + System.Environment.NewLine);
            for (int i = 0; i < localColumnCount; i++)
            {
                _sb.Append(conflictingLocalChange.Rows[0][i] + "| ");
            }

            _sb.Append(System.Environment.NewLine + "Row from OtherDB " + System.Environment.NewLine);
            for (int i = 0; i < remoteColumnCount; i++)
            {
                _sb.Append(conflictingRemoteChange.Rows[0][i] + "| ");
            }


            e.Action = ApplyAction.RetryWithForceWrite;

            using (var _sqlServerConn = new SqlConnection(LocalConnectionString))
            {
                var _cmdInsLog = new SqlCommand("SyncErrorLog_INSERT", _sqlServerConn);
                _cmdInsLog.CommandType = CommandType.StoredProcedure;
                _cmdInsLog.Parameters.Add(new SqlParameter("@ConflitType", e.Conflict.Type.ToString()));
                _cmdInsLog.Parameters.Add(new SqlParameter("@Message", (e.Conflict.ErrorMessage != null) ? e.Conflict.ErrorMessage : String.Empty));
                _cmdInsLog.Parameters.Add(new SqlParameter("@DbConflictDetected", e.Connection.Database.ToString()));
                _cmdInsLog.Parameters.Add(new SqlParameter("@EventDateTime", DateTime.Now.ToString()));
                _cmdInsLog.Parameters.Add(new SqlParameter("@Detail", _sb.ToString()));

                if (_sqlServerConn.State != ConnectionState.Open)
                    _cmdInsLog.Connection.Open();
                _cmdInsLog.ExecuteNonQuery();

                _cmdInsLog.Connection.Close();
            }
        }

        #endregion

        #region == Event Handlers =========================
        /// <summary>
        ///     Called when a change from the local database is in conflict with a change from the Azure database.
        ///     To override the sync framework's default behavior, the event handler should set e.Action to the desired action.
        /// </summary>
        public event EventHandler<DbApplyChangeFailedEventArgs> LocalApplyChangeFailed;

        /// <summary>
        ///     Called when a change from the Azure database is in conflict with a change from the local database.
        ///     To override the sync framework's default behavior, the event handler should set e.Action to the desired action.
        /// </summary>
        public event EventHandler<DbApplyChangeFailedEventArgs> AzureApplyChangeFailed;
        #endregion

    }
}
